/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.query;

import java.io.OutputStream;
import java.util.*;

import org.apache.jena.atlas.io.IndentedLineBuffer;
import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.atlas.io.Printable;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.graph.Node;
import org.apache.jena.sparql.ARQConstants;
import org.apache.jena.sparql.algebra.table.TableData;
import org.apache.jena.sparql.core.*;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.expr.ExprAggregator;
import org.apache.jena.sparql.expr.ExprTransform;
import org.apache.jena.sparql.expr.ExprVar;
import org.apache.jena.sparql.expr.aggregate.Aggregator;
import org.apache.jena.sparql.serializer.QuerySerializerFactory;
import org.apache.jena.sparql.serializer.SerializerRegistry;
import org.apache.jena.sparql.syntax.Element;
import org.apache.jena.sparql.syntax.PatternVars;
import org.apache.jena.sparql.syntax.Template;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformCopyBase;
import org.apache.jena.sparql.syntax.syntaxtransform.ExprTransformApplyElementTransform;
import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps;
import org.apache.jena.sparql.util.FmtUtils;
import org.apache.jena.sys.JenaSystem;

/** The data structure for a query as presented externally.
 *  There are two ways of creating a query - use the parser to turn
 *  a string description of the query into the executable form, and
 *  the programmatic way (the parser is calling the programmatic
 *  operations driven by the query string).  The declarative approach
 *  of passing in a string is preferred.
 *
 * Once a query is built, it can be passed to the QueryFactory to produce a query execution engine.
 * @see QueryExecutionFactory
 * @see ResultSet
 */

public class Query extends Prologue implements Cloneable, Printable
{
    static { JenaSystem.init(); /* Ensure everything has started properly */ }

    // Old constants. Retain for compatibility.
    public static final int QueryTypeUnknown    = -123;
    public static final int QueryTypeSelect     = 111;
    public static final int QueryTypeConstruct  = 222;
    public static final int QueryTypeDescribe   = 333;
    public static final int QueryTypeAsk        = 444;
    public static final int QueryTypeJson       = 555;

    private QueryType queryType = QueryType.UNKNOWN;

    // If no model is provided explicitly, the query engine will load
    // a model from the URL.  Never a list of zero items.

    private List<String> graphURIs = new ArrayList<>();
    private List<String> namedGraphURIs = new ArrayList<>();

    // The WHERE clause
    private Element queryPattern = null;

    // Query syntax
    private Syntax syntax = Syntax.syntaxSPARQL; // Default

    // LIMIT/OFFSET
    public static final long  NOLIMIT = Long.MIN_VALUE;
    private long resultLimit   = NOLIMIT;
    private long resultOffset  = NOLIMIT;

    // ORDER BY
    private List<SortCondition> orderBy       = null;
    public static final int ORDER_ASCENDING           = 1;
    public static final int ORDER_DESCENDING          = -1;
    public static final int ORDER_DEFAULT             = -2;    // Not explicitly given.
    public static final int ORDER_UNKNOW              = -3;

    // VALUES trailing clause
    protected TableData valuesDataBlock = null;

    protected boolean strictQuery = true;

    // SELECT * seen
    protected boolean queryResultStar        = false;

    protected boolean distinct               = false;
    protected boolean reduced                = false;

    // CONSTRUCT
    protected Template constructTemplate  = null;

    // DESCRIBE
    // Any URIs/QNames in the DESCRIBE clause
    // Also uses resultVars
    protected List<Node> resultNodes               = new ArrayList<>();     // Type in list: Node

    /**
     * Creates a new empty query
     */
    public Query() {
        syntax = Syntax.syntaxSPARQL;
    }

    /**
     * Creates a new empty query with the given prologue
     */
    public Query(Prologue prologue) {
        this();
        Prologue p2 = prologue.copy();
        prefixMap = p2.getPrefixMapping();
        seenBaseURI = false;
        resolver = p2.getResolver();
    }

    // Allocate variables that are unique to this query.
    private VarAlloc varAlloc = new VarAlloc(ARQConstants.allocQueryVariables);
    private Var allocInternVar() { return varAlloc.allocVar(); }

    public void setQuerySelectType()            { queryType = QueryType.SELECT; }
    public void setQueryConstructType()         { queryType = QueryType.CONSTRUCT; queryResultStar = true; }
    public void setQueryDescribeType()          { queryType = QueryType.DESCRIBE; }
    public void setQueryAskType()               { queryType = QueryType.ASK; }
    public void setQueryJsonType()              { queryType = QueryType.CONSTRUCT_JSON; }

    /** Return the {@link QueryType} */
    public QueryType queryType()                { return queryType; }

    public boolean isSelectType()               { return queryType == QueryType.SELECT; }

    public boolean isConstructType()            { return queryType == QueryType.CONSTRUCT; }

    public boolean isDescribeType()             { return queryType == QueryType.DESCRIBE; }

    public boolean isAskType()                  { return queryType == QueryType.ASK; }

    public boolean isJsonType()                 { return queryType == QueryType.CONSTRUCT_JSON; }

    public boolean isUnknownType()              { return queryType == QueryType.UNKNOWN; }

    public boolean isConstructQuad() {
        return (isConstructType() && constructTemplate.containsRealQuad());
    }

    // It was a mistake to extend Prologue ... but what is done is done.
    public Prologue getPrologue()               { return this; }

    public void setStrict(boolean isStrict) {
        strictQuery = isStrict;
    }

    public boolean isStrict()                { return strictQuery; }

    public void setDistinct(boolean b) { distinct = b; }
    public boolean isDistinct()        { return distinct; }

    public void setReduced(boolean b) { reduced = b; }
    public boolean isReduced()        { return reduced; }

    /** @return Returns the syntax. */
    public Syntax getSyntax()         { return syntax; }

    /** @param syntax The syntax to set. */
    public void setSyntax(Syntax syntax) {
        this.syntax = syntax;
        if ( syntax != Syntax.syntaxSPARQL )
            strictQuery = false;
    }

    // ---- Limit/offset

    public long getLimit()             { return resultLimit; }
    public void setLimit(long limit)   { resultLimit = limit; }
    public boolean hasLimit()          { return resultLimit != NOLIMIT; }

    public long getOffset()            { return resultOffset; }
    public void setOffset(long offset) { resultOffset = offset; }
    public boolean hasOffset()         { return resultOffset != NOLIMIT; }

    // ---- Order By

    public boolean hasOrderBy()        { return orderBy != null && orderBy.size() > 0; }

    public boolean isOrdered()         { return hasOrderBy(); }

    public void addOrderBy(SortCondition condition) {
        if ( orderBy == null )
            orderBy = new ArrayList<>();

        orderBy.add(condition);
    }

    public void addOrderBy(Expr expr, int direction) {
        SortCondition sc = new SortCondition(expr, direction);
        addOrderBy(sc);
    }

    public void addOrderBy(Node var, int direction) {
        if ( !var.isVariable() )
            throw new QueryException("Not a variable: " + var);
        SortCondition sc = new SortCondition(var, direction);
        addOrderBy(sc);
    }

    public void addOrderBy(String varName, int direction) {
        varName = Var.canonical(varName);
        SortCondition sc = new SortCondition(new ExprVar(varName), direction);
        addOrderBy(sc);
    }

    public List<SortCondition> getOrderBy() {
        return orderBy;
    }

    // ----

    /**
     * Answer whether the query had SELECT/DESCRIBE/CONSTRUCT *
     *
     * @return boolean as to whether a * result form was seen
     */
    public boolean isQueryResultStar() {
        return queryResultStar;
    }

    /**
     * Set whether the query had SELECT/DESCRIBE *
     *
     * @param isQueryStar
     */
    public void setQueryResultStar(boolean isQueryStar) {
        queryResultStar = isQueryStar;
        if ( isQueryStar )
            resultVarsSet = false;
    }

    public void setQueryPattern(Element elt) {
        queryPattern = elt;
    }

    public Element getQueryPattern() {
        return queryPattern;
    }

    /**
     * Location of the source for the data. If the model is not set, then the
     * QueryEngine will attempt to load the data from these URIs into the default
     * (unnamed) graph.
     */
    public void addGraphURI(String s) {
        if ( graphURIs == null )
            graphURIs = new ArrayList<>();
        graphURIs.add(s);
    }

    /**
     * Location of the source for the data. If the model is not set, then the
     * QueryEngine will attempt to load the data from these URIs as named graphs in
     * the dataset.
     */
    public void addNamedGraphURI(String uri) {
        if ( namedGraphURIs == null )
            namedGraphURIs = new ArrayList<>();
        if ( namedGraphURIs.contains(uri) )
            throw new QueryException("URI already in named graph set: " + uri);
        else
            namedGraphURIs.add(uri);
    }

    /**
     * Return the list of URIs (strings) for the unnamed graph
     *
     * @return List of strings
     */
    public List<String> getGraphURIs() { return graphURIs; }

    /** Test whether the query mentions a URI in forming the default graph (FROM clause)
     *
     * @param uri
     * @return boolean  True if the URI used in a FROM clause
     */
    public boolean usesGraphURI(String uri) { return graphURIs.contains(uri); }

    /**
     * Return the list of URIs (strings) for the named graphs (FROM NAMED clause)
     *
     * @return List of strings
     */
    public List<String> getNamedGraphURIs() { return namedGraphURIs; }

    /**
     * Test whether the query mentions a URI for a named graph.
     *
     * @param uri
     * @return True if the URI used in a FROM NAMED clause
     */
    public boolean usesNamedGraphURI(String uri) { return namedGraphURIs.contains(uri); }

    /**
     * Return true if the query has either some graph URIs or some named graph URIs
     * in its description. This does not mean these URIs will be used - just that
     * they are noted as part of the query.
     */

    public boolean hasDatasetDescription() {
        if ( getGraphURIs() != null && getGraphURIs().size() > 0 )
            return true;
        if ( getNamedGraphURIs() != null && getNamedGraphURIs().size() > 0 )
            return true;
        return false;
    }

    /** Return a dataset description (FROM/FROM NAMED clauses) for the query. */
    public DatasetDescription getDatasetDescription() {
        if ( !hasDatasetDescription() )
            return null;

        DatasetDescription description = new DatasetDescription();

        description.addAllDefaultGraphURIs(getGraphURIs());
        description.addAllNamedGraphURIs(getNamedGraphURIs());
        return description;
    }

    // ---- SELECT

    protected VarExprList projectVars = new VarExprList();

    /** Return a list of the variables requested (SELECT) */
    public List<String> getResultVars() {
        // Ensure "SELECT *" processed
        ensureResultVars();
        return Var.varNames(projectVars.getVars());
    }

    /** Return a list of the variables requested (SELECT) */
    public List<Var> getProjectVars() {
        // Ensure "SELECT *" processed
        ensureResultVars();
        return projectVars.getVars();
    }

    public VarExprList getProject() {
        return projectVars;
    }

    /** Add a collection of projection variables to a SELECT query */
    public void addProjectVars(Collection<? > vars) {
        for ( Object obj : vars ) {
            if ( obj instanceof String s ) {
                addResultVar(s);
                continue;
            }
            if ( obj instanceof Var v ) {
                addResultVar(v);
                continue;
            }
            throw new QueryException("Not a variable or variable name: " + obj);
        }
        resultVarsSet = true;
    }

    /**
     * Are the projectVars set?
     * This can happen incrementally (the syntax has explicitly named variables e.g. {@code SELECT ?x`})
     * or by analysis {@code SELECT *}.
     */
    private boolean resultVarsSet = false;

    public void ensureResultVars() {
        if ( resultVarsSet )
            return;
        _resetResultVars();
    }

    /**
     * If modifying a query, it may be necessary to reset the calculated result
     * variables of the query for {@code SELECT *} and {@code DESCRIBE *} and
     * {@code CONSTRUCT}.
     */
    public void resetResultVars() {
        _resetResultVars();
    }

    private void _resetResultVars() {
        if ( isQueryResultStar() )
            projectVars.clear();

        if ( getQueryPattern() == null ) {
            if ( !this.isDescribeType() )
                Log.warn(this, "No query pattern in non-DESCRIBE query");
            resultVarsSet = true;
            return;
        }

        _findAndAddNamedVars();
        resultVarsSet = true;
    }

    private void _findAndAddNamedVars() {
        Iterator<Var> varIter = null;
        if ( isQueryResultStar() ) {
            if ( hasGroupBy() ) {
                varIter = groupVars.getVars().iterator();
            } else {
                // Binding variables -- in patterns, not in filters and not in EXISTS
                LinkedHashSet<Var> queryVars = new LinkedHashSet<>();
                PatternVars.vars(queryVars, this.getQueryPattern());
                if ( this.hasValues() )
                    queryVars.addAll(getValuesVariables());
                varIter = queryVars.iterator();
            }

        } else {
            varIter = projectVars.getVars().iterator();
        }

        // All query variables, including ones from bNodes in the query.

        for ( ; varIter.hasNext() ; ) {
            Var var = varIter.next();
            if ( var.isNamedVar() && ! projectVars.contains(var) ) {
                addResultVar(var);
            }
        }
    }

    /** Add a projection variable to a SELECT query */
    public void addResultVar(String varName) {
        varName = Var.canonical(varName);
        _addResultVar(varName);
    }

    public void addResultVar(Node v) {
        if ( !v.isVariable() )
            throw new QueryException("Not a variable: " + v);
        _addResultVar(v.getName());
    }

    public void addResultVar(Node v, Expr expr) {
        Var var = null;
        if ( v == null )
            var = allocInternVar();
        else {
            if ( !v.isVariable() )
                throw new QueryException("Not a variable: " + v);
            var = Var.alloc(v);
        }
        _addVarExpr(projectVars, var, expr);
    }

    /** Add an to a SELECT query (a name will be created for it) */
    public void addResultVar(Expr expr) {
        _addVarExpr(projectVars, allocInternVar(), expr);
    }

    /** Add a named expression to a SELECT query */
    public void addResultVar(String varName, Expr expr) {
        Var var = null;
        if ( varName == null )
            var = allocInternVar();
        else {
            varName = Var.canonical(varName);
            var = Var.alloc(varName);
        }
        _addVarExpr(projectVars, var, expr);
    }

    // Add raw name.
    private void _addResultVar(String varName) {
        Var v = Var.alloc(varName);
        _addVar(projectVars, v);
        resultVarsSet = true;
    }

    private static void _addVar(VarExprList varExprList, Var v) {
        if ( varExprList.contains(v) ) {
            Expr expr = varExprList.getExpr(v);
            if ( expr != null )
                // SELECT (?a+?b AS ?x) ?x
                throw new QueryBuildException("Duplicate variable (had an expression) in result projection '" + v + "'");
            // SELECT ?x ?x
            return;
        }
        varExprList.add(v);
    }

    private void _addVarExpr(VarExprList varExprList, Var v, Expr expr) {
        if ( varExprList.contains(v) )
            // SELECT ?x (?a+?b AS ?x)
            // SELECT (2*?a AS ?x) (?a+?b AS ?x)
            throw new QueryBuildException("Duplicate variable in result projection '" + v + "'");
        varExprList.add(v, expr);
        resultVarsSet = true;
    }

    protected VarExprList groupVars = new VarExprList();
    protected List<Expr> havingExprs = new ArrayList<>();  // Expressions : Make an ExprList?

    public boolean hasGroupBy()     { return ! groupVars.isEmpty() || getAggregators().size() > 0; }
    public boolean hasHaving()      { return havingExprs != null && havingExprs.size() > 0; }

    public VarExprList getGroupBy()      { return groupVars; }

    public List<Expr> getHavingExprs()    { return havingExprs; }

    public void addGroupBy(String varName)
    {
        varName = Var.canonical(varName);
        addGroupBy(Var.alloc(varName));
    }

    public void addGroupBy(Node v) {
        _addVar(groupVars, Var.alloc(v));
    }

    public void addGroupBy(Expr expr) { addGroupBy(null, expr); }

    public void addGroupBy(Var v, Expr expr) {
        if ( v == null )
            v = allocInternVar();

        if ( expr.isVariable() && v.isAllocVar() ) {
            // It was (?x) with no AS - keep the name by adding by variable.
            addGroupBy(expr.asVar());
            return;
        }
        groupVars.add(v, expr);
    }

    public void addHavingCondition(Expr expr) {
        havingExprs.add(expr);
    }

    // SELECT JSON

    private Map<String, Node> jsonMapping = new LinkedHashMap<>();

    public void addJsonMapping(String key, Node value) {
        jsonMapping.put(key, value);
    }

    public Map<String, Node> getJsonMapping() {
        return Collections.unmodifiableMap(jsonMapping);
    }

    /**
     * Overwrite all prior JSON mappings with new ones.
     *
     * @param newJsonMapping The new JSON mappings. Must not be null.
     */
    public void setJsonMapping(Map<String, Node> newJsonMapping) {
        Objects.requireNonNull(newJsonMapping);
        jsonMapping.clear();
        jsonMapping.putAll(newJsonMapping);
    }

    // ---- Aggregates

    // Record allocated aggregations.
    // Later: The same aggregation expression used in a query
    // will always lead to the same aggregator.
    // For now, allocate a fresh one each time (cause the calculation
    // to be done multiple times but (1) it's unusual to have repeated
    // aggregators normally and (2) the actual calculation is cheap.

    // Unlike SELECT expressions, here the expression itself (E_Aggregator) knows its variable
    // Commonality?

    private List<ExprAggregator> aggregators = new ArrayList<>();
    private Map<Var, ExprAggregator> aggregatorsMap = new HashMap<>();

    // Note any E_Aggregator created for reuse.
    private Map<String, Var> aggregatorsAllocated = new HashMap<>();

    public boolean hasAggregators() { return aggregators.size() != 0 ; }
    public List<ExprAggregator> getAggregators() { return aggregators; }

    public Expr allocAggregate(Aggregator agg) {
        // We need to track the aggregators in case one aggregator is used twice,
        // e.g. in HAVING and in SELECT expression
        // (is that much harm to do twice? Yes, if distinct.)
        String key = agg.key();

        Var v = aggregatorsAllocated.get(key);
        if ( v != null ) {
            ExprAggregator eAgg = aggregatorsMap.get(v);
            if ( !agg.equals(eAgg.getAggregator()) )
                Log.warn(Query.class, "Internal inconsistency: Aggregator: " + agg);
            return eAgg;
        }
        // Allocate.
        v = allocInternVar();
        ExprAggregator aggExpr = new ExprAggregator(v, agg);
        aggregatorsAllocated.put(key, v);
        aggregatorsMap.put(v, aggExpr);
        aggregators.add(aggExpr);
        return aggExpr;
    }

    // ---- VALUES

    /** Does the query have a VALUES trailing block? */
    public boolean hasValues()                { return valuesDataBlock != null; }

    /** Variables from a VALUES trailing block */
    public List<Var> getValuesVariables()     { return valuesDataBlock==null ? null : valuesDataBlock.getVars(); }

    /** Data from a VALUES trailing block. null for a Node means undef */
    public List<Binding> getValuesData()      { return valuesDataBlock==null ? null : valuesDataBlock.getRows(); }

    public void setValuesDataBlock(List<Var> variables, List<Binding> values) {
        checkDataBlock(variables, values);
        valuesDataBlock = new TableData(variables, values);
    }

    private static void checkDataBlock(List<Var> variables, List<Binding> values) {
        // Check.
        int N = variables.size();
        for ( Binding valueRow : values ) {
            Iterator<Var> iter = valueRow.vars();
            for ( ; iter.hasNext() ; ) {
                Var v = iter.next();
                if ( !variables.contains(v) )
                    throw new QueryBuildException("Variable " + v + " not found in " + variables);
            }
        }
    }

    // ---- CONSTRUCT

    /** Get the template pattern for a construct query */
    public Template getConstructTemplate() {
        return constructTemplate;
    }

    /** Set triple patterns for a construct query */
    public void setConstructTemplate(Template templ) {
        constructTemplate = templ;
    }

    // ---- DESCRIBE

    public void addDescribeNode(Node node) {
        if ( node.isVariable() ) {
            addResultVar(node);
            return;
        }
        if ( node.isURI() || node.isBlank() ) {
            if ( !resultNodes.contains(node) )
                resultNodes.add(node);
            return;
        }
        if ( node.isLiteral() )
            throw new QueryException("Result node is a literal: " + FmtUtils.stringForNode(node));
        throw new QueryException("Result node not recognized: " + node);
    }

    /**
     * Get the result list URI of a DESCRIBE query.
     */
    public List<Node> getResultURIs() { return resultNodes; }

    public void visit(QueryVisitor visitor) {
        visitor.startVisit(this);
        visitor.visitResultForm(this);
        visitor.visitPrologue(this);
        if ( this.isSelectType() )
            visitor.visitSelectResultForm(this);
        if ( this.isConstructType() )
            visitor.visitConstructResultForm(this);
        if ( this.isDescribeType() )
            visitor.visitDescribeResultForm(this);
        if ( this.isAskType() )
            visitor.visitAskResultForm(this);
        if ( this.isJsonType() )
            visitor.visitJsonResultForm(this);
        visitor.visitDatasetDecl(this);
        visitor.visitQueryPattern(this);
        visitor.visitGroupBy(this);
        visitor.visitHaving(this);
        visitor.visitOrderBy(this);
        visitor.visitOffset(this);
        visitor.visitLimit(this);
        visitor.visitValues(this);
        visitor.finishVisit(this);
    }

    @Override
    public Object clone() { return cloneQuery(); }

    /**
     * Makes a copy of this query using the syntax transform machinery.
     * @return Copy of this query
     */
    public Query cloneQuery() {
        ElementTransform eltTransform = new ElementTransformCopyBase(true);
        ExprTransform exprTransform = new ExprTransformApplyElementTransform(eltTransform, true);

        Query result = QueryTransformOps.transform(this, eltTransform, exprTransform);
        return result;
    }

    // ---- Query canonical syntax

    // Reverse of parsing : should produce a string that parses to an equivalent query
    // "Equivalent" => gives the same results on any model
    @Override
    public String toString()
    { return serialize(); }

    public String toString(Syntax syntax)
    { return serialize(syntax); }

    /** Must align with .equals */
    private int hashcode = -1;

    @Override
    public int hashCode() {
        if ( hashcode == -1 ) {
            hashcode = QueryHashCode.calc(this);
            if ( hashcode == -1 )
                hashcode = Integer.MIN_VALUE / 2;
        }
        return hashcode;
    }

    /** Are two queries equals - tests shape and details.
     * Equality means that the queries do the same thing, including
     * same variables, in the same places.  Being unequals does
     * <b>not</b> mean the queries do different things.
     *
     * For example, reordering a group or union
     * means that a query is different.
     *
     * Two instances of a query parsed from the same string are equal.
     */
    @Override
    public boolean equals(Object other) {
        if ( !(other instanceof Query) )
            return false;
        if ( this == other )
            return true;
        return QueryCompare.equals(this, (Query)other);
    }

    @Override
    public void output(IndentedWriter out) {
        serialize(out);
    }

    /** Convert the query to a string */
    public String serialize() {
        IndentedLineBuffer buff = new IndentedLineBuffer();
        serialize(buff);
        return buff.toString();
    }

    /**
     * Convert the query to a string in the given syntax
     *
     * @param syntax
     */
    public String serialize(Syntax syntax) {
        IndentedLineBuffer buff = new IndentedLineBuffer();
        serialize(buff, syntax);
        return buff.toString();
    }

    /** Output the query
     * @param out  OutputStream
     */
    public void serialize(OutputStream out) { serialize(out, syntax); }

    /** Output the query
     *
     * @param out     OutputStream
     * @param syntax  Syntax URI
     */
    public void serialize(OutputStream out, Syntax syntax) {
        IndentedWriter writer = new IndentedWriter(out);
        serialize(writer, syntax);
        writer.flush();
        try { out.flush(); } catch (Exception ex) { }
    }

    /**
     * Format the query
     *
     * @param writer IndentedWriter
     */
    public void serialize(IndentedWriter writer) {
        serialize(writer, syntax);
    }

    /**
     * Format the query
     *
     * @param writer IndentedWriter
     * @param outSyntax Syntax URI
     */
    public void serialize(IndentedWriter writer, Syntax outSyntax) {
        // Try to use a serializer factory if available
        QuerySerializerFactory factory = SerializerRegistry.get().getQuerySerializerFactory(outSyntax);
        QueryVisitor serializer = factory.create(outSyntax, this, writer);
        this.visit(serializer);
    }
}
